{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Parte 08 - Introducción a los Planes \n",
    "\n",
    "\n",
    "### Contexto\n",
    "\n",
    "Aquí introducimos un objeto crucial para escalar industrialmente el Aprendizaje Federado: El Plan. Reduce dramáticamente el uso de ancho de banda, permite esquemas asíncronos y da más autonomía a dispositivos remotos. El concepto original del Plan puede ser encontrado en el artículo [Towards Federated Learning at Scale: System Design](https://arxiv.org/pdf/1902.01046.pdf), pero ha sido adaptado a nuestras necesitades en la librería Pysyft.\n",
    "\n",
    "La intención de un Plan es almacenar una secuencia de operaciones torch, igual que una función, pero permitiendo enviar dicha secuencia de operaciones a trabajadores remotos y mantener una referencia a el. De esta manera, para computar remotamente la secuencia de $n$ operaciones en una entrada remota referenciada a través de punteros, en lugar de enviar $n$ mensajes ahora debes enviar un solo mensaje con las referencias al Plan y los punteros. También puedes proveer tensores con tu función (los que llamamos _state tensors_) para tener funcionalidades extendidas. Los Planes pueden ser vistos como una función que puedes enviar o como una clase que puede ser enviada y ejecutada remotamente. Por lo tanto, para usuarios de alto nivel, la noción de un Plan desaparece y es reemplazada por una característica mágica que permite permite enviar a trabajadores funciones arbitrarias que contienen funciones secuenciales torch.\n",
    "\n",
    "Algo para notar es que la clase de funciones que puedes transformar en Planes está actualmente limitada a secuencias de operaciones torch enganchadas. Esto excluye en particular a estructuras lógicas como declaraciones de `if`, `for` y `while`, aunque estamos trabajando para tener soluciones alternativas pronto. _Para ser completamente precisos, puedes usarlos pero el camino lógico que tomes (el primer `if` que resulte False y 5 bucles en `for`, por ejemplo) en la primer computación de tu Plan será en la que seguirás durante todas las siguientes computaciones, cosa que queremos evitar en la mayoría de los casos._\n",
    "\n",
    " Autores:\n",
    "- Théo Ryffel - Twitter [@theoryffel](https://twitter.com/theoryffel) - GitHub: [@LaRiffle](https://github.com/LaRiffle)\n",
    "- Bobby Wagner - Twitter [@bobbyawagner](https://twitter.com/bobbyawagner) - GitHub: [@robert-wagner](https://github.com/robert-wagner)\n",
    "- Marianne Monteiro - Twitter [@hereismari](https://twitter.com/hereismari) - GitHub: [@mari-linhares](https://github.com/mari-linhares)\t\n",
    "\n",
    " Traducción:\n",
    "\n",
    "- Arturo Márquez Flores - Twitter: [@arturomf94](https://twitter.com/arturomf94) \n",
    "- Ricardo Pretelt - Twitter: [@ricardopretelt](https://twitter.com/ricardopretelt)\n",
    "- Carlos Salgado - Github: [@socd06](https://github.com/socd06) "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Importaciones y especificaciones para el modelo\n",
    "\n",
    "Primero hagamos las importaciones oficiales."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import torch\n",
    "import torch.nn as nn\n",
    "import torch.nn.functional as F"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Note que en las importaciones específicas a Pysyft: **el trabajador local no debe ser un trabajador de cliente.** *Trabajadores que no son de cliente pueden almacenar objetos y necesitamos de eso para correr un Plan.*"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import syft as sy  # importar la librería Pysyft\n",
    "hook = sy.TorchHook(torch)  # enganchar PyTorch (por ejemplo para agregar funcionalidad extra)\n",
    "\n",
    "# IMPORTANTE: El trabajador local no debe ser un trabajador de cliente\n",
    "hook.local_worker.is_client_worker = False\n",
    "\n",
    "\n",
    "server = hook.local_worker"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Definimos trabajadores remotos o _devices_, para que sean consistentes con las nociones provistas en el artículo de referencia. \n",
    "Los proveemos de unos datos."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "x11 = torch.tensor([-1, 2.]).tag('input_data')\n",
    "x12 = torch.tensor([1, -2.]).tag('input_data2')\n",
    "x21 = torch.tensor([-1, 2.]).tag('input_data')\n",
    "x22 = torch.tensor([1, -2.]).tag('input_data2')\n",
    "\n",
    "device_1 = sy.VirtualWorker(hook, id=\"device_1\", data=(x11, x12)) \n",
    "device_2 = sy.VirtualWorker(hook, id=\"device_2\", data=(x21, x22))\n",
    "devices = device_1, device_2"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Ejemplo Básico\n",
    "\n",
    "Definamos una función que queremos transformar en un Plan. Hacerlo es tán simple como agregarle un decorador encima de la definición de la función."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "@sy.func2plan()\n",
    "def plan_double_abs(x):\n",
    "    x = x + x\n",
    "    x = torch.abs(x)\n",
    "    return x"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Revisemos, ¡ahora tenemos un plan!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plan_double_abs"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Para usar un Plan, necesitas dos cosas: construir el Plan (_por ejemplo, registrar la secuencia de operaciones presentes en la función_) y enviarlo al trabajador / dispositivo. Afortunadamente puedes hacer esto muy fácilmente.\n",
    "\n",
    "#### Construir un Plan\n",
    "\n",
    "Para construir un Plan solo necesitas llamarlo con algo de datos.\n",
    "\n",
    "Primero consigamos la referencia para datos remotos: una requisición es enviada a la red y el puntero de referencia es regresado."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pointer_to_data = device_1.search('input_data')[0]\n",
    "pointer_to_data"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si le decimos al plan que debe ser ejecutado remotamente en el dispositivo `location:device_1`... obtendremos un error porque el Plan no ha sido construido todavía."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plan_double_abs.is_built"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Enviar un Plan no construido fallará\n",
    "try:\n",
    "    plan_double_abs.send(device_1)\n",
    "except RuntimeError as error:\n",
    "    print(error)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Para construir un Plan solo necesitas llamar a `build` en el Plan y pasar los argumentos necesesarios para ejecutar el Plan (a.k.a unos datos). Cuando el Plan es construido, todos los comandos son ejecutados secuencialmente por el trabajador local,  son atrapados por el Plan y almacenados en el atributo `actions`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plan_double_abs.build(torch.tensor([1., -2.]))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plan_double_abs.is_built"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahora ya podemos enviar el plan"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Ahora podemos ejecutar exitosamente esta celda\n",
    "pointer_plan = plan_double_abs.send(device_1)\n",
    "pointer_plan"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Así como con los tensores, obtenemos un puntero al objeto enviado. Se le llama simplemente un `PointerPlan`"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Algo importante a recordar es que cuando un Plan es construido, predefinimos las id(s) donde el resultado(s) debe ser almacenado antes de hacer la computación. Esto permitirá enviar comandos asíncronamente, para ya tener una referencia a un resultado virtual y continuar las computaciones locales sin esperar que el resultado remoto. Una aplicación importante es cuando quieras hacer un cómputo de un grupo de datos en device_1 y no quieras esperar a que la computación termine para lanzar otro cómputo de grupo en device_2."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Correr un Plan remotamente\n",
    "\n",
    "Ahora podemos correr el Plan remotamente llamando el puntero al plan con un puntero a unos datos. Esto emite un comando a correr el plan remotamente, para que la localización predefinida de la salida del plan ahora contenga el resultado (recuerda que predefinimos la localización del resultado antes de la computación). Esto tambien necesita solo una ronda de comunicación.\n",
    "\n",
    "El resultado es simplemente un puntero, igual al que llamas cuando usas una función torch enganchada."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pointer_to_result = pointer_plan(pointer_to_data)\n",
    "print(pointer_to_result)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y solo pedimos el valor de vuelta"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pointer_to_result.get()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Hacia un ejemplo concreto\n",
    "\n",
    "¿Queremos aplicar los Planes para el Aprendizaje Federado y Profundo, verdad? \n",
    "Veamos un ejemplo ligeramente más complicado, usando redes neuronales como quisieras hacerlo.\n",
    "Nota que ahora transformamos una clase en un Plan. Para hacerlo, heredamos nuestra clase de sy.Plan (en lugar de heredarla de nn.Module)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Net(sy.Plan):\n",
    "    def __init__(self):\n",
    "        super(Net, self).__init__()\n",
    "        self.fc1 = nn.Linear(2, 3)\n",
    "        self.fc2 = nn.Linear(3, 2)\n",
    "\n",
    "    def forward(self, x):\n",
    "        x = F.relu(self.fc1(x))\n",
    "        x = self.fc2(x)\n",
    "        return F.log_softmax(x, dim=0)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "net = Net()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "net"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Construyamos el plan usando datos simulados. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "net.build(torch.tensor([1., 2.]))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahora enviamos el Plan a un trabajador remoto"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pointer_to_net = net.send(device_1)\n",
    "pointer_to_net"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Recuperemos unos datos remotos"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pointer_to_data = device_1.search('input_data')[0]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Después, la sintaxis es igual que la ejecución secuencial remota normal, igual que la ejecución local. Pero comparada a la ejecución remota, hay solo una ronda de comunicación por cada ejecución."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pointer_to_result = pointer_to_net(pointer_to_data)\n",
    "pointer_to_result"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y obtenemos los resultados como de costumbre"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pointer_to_result.get()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Et voilà! Hemos visto como reducir dramáticamente la comunicación entre el trabajador local (o servidor) y los dispositivos remotos."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Cambiar de trabajadores\n",
    "\n",
    "Una característica importante que queremos tener es usar el mismo Plan para varios trabajadores, que cambiemos dependiendo del grupo remoto de datos que estemos considerando. \n",
    "\n",
    "En particular, no queremos reconstruir el Plan cada vez que cambiemos de trabajador. Veamos como hacer esto, usando el ejemplo anterior con nuestra pequeña red."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Net(sy.Plan):\n",
    "    def __init__(self):\n",
    "        super(Net, self).__init__()\n",
    "        self.fc1 = nn.Linear(2, 3)\n",
    "        self.fc2 = nn.Linear(3, 2)\n",
    "\n",
    "    def forward(self, x):\n",
    "        x = F.relu(self.fc1(x))\n",
    "        x = self.fc2(x)\n",
    "        return F.log_softmax(x, dim=0)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "net = Net()\n",
    "\n",
    "# Construir el Plan\n",
    "net.build(torch.tensor([1., 2.]))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Aqui están los pasos principales que acabamos de ejecutar"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pointer_to_net_1 = net.send(device_1)\n",
    "pointer_to_data = device_1.search('input_data')[0]\n",
    "pointer_to_result = pointer_to_net_1(pointer_to_data)\n",
    "pointer_to_result.get()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y de hecho, puedes construir otros PointerPlans del mismo Plan, dado que la sintaxis es la misma para correr remotamente un plan en otro dispositivo."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pointer_to_net_2 = net.send(device_2)\n",
    "pointer_to_data = device_2.search('input_data')[0]\n",
    "pointer_to_result = pointer_to_net_2(pointer_to_data)\n",
    "pointer_to_result.get()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "> Nota: Actualmente, con clases Plan, solo puedes usar un método y lo tienes que llamar \"forward\"."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Construir Planes que son funciones automáticamente \n",
    "\n",
    "Para funciones (`@` `sy.func2plan`) que podemos construir automáticamente sin necesidad de explícitamente llamar `build`, en el momento de la creación el Plan ya está construido.\n",
    "\n",
    "Para conseguir esta funcionalidad lo único que necesitas cambiar cuando creas el Plan es configurar un argumento para el decorador llamado `args_shape` el cual debe ser una lista conteniendo las formas de cada argumento."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "@sy.func2plan(args_shape=[(-1, 1)])\n",
    "def plan_double_abs(x):\n",
    "    x = x + x\n",
    "    x = torch.abs(x)\n",
    "    return x\n",
    "\n",
    "plan_double_abs.is_built"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El parámetro `args_shape` se usa interamente para crear tensores simulados con la forma dada que son usados para construir el Plan."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "@sy.func2plan(args_shape=[(1, 2), (-1, 2)])\n",
    "def plan_sum_abs(x, y):\n",
    "    s = x + y\n",
    "    return torch.abs(s)\n",
    "\n",
    "plan_sum_abs.is_built"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "¡También puedes darle elementos de estado a las funciones!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "@sy.func2plan(args_shape=[(1,)], state=(torch.tensor([1]), ))\n",
    "def plan_abs(x, state):\n",
    "    bias, = state.read()\n",
    "    x = x.abs()\n",
    "    return x + bias"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pointer_plan = plan_abs.send(device_1)\n",
    "x_ptr = torch.tensor([-1, 0]).send(device_1)\n",
    "p = pointer_plan(x_ptr)\n",
    "p.get()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Para aprender más al respecto, puedes descubrir como usamos los Planes con Protocolos en el Tutorial Parte 08 Bis."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Dale una Estrella a PySyft en Github\n",
    "\n",
    "¡La forma más fácil de ayudar a nuestra comunidad es guardando con una estrella los Repos! Esto ayuda a crear consciencia de las geniales herramientas que estamos construyendo.\n",
    "\n",
    "- [Guardar con Estrella a PySyft](https://github.com/OpenMined/PySyft)\n",
    "\n",
    "### ¡Únete a nuestro Slack!\n",
    "\n",
    "¡La mejor manera de estar al día con los últimos avances es unirte a nuestra comunidad! Puedes hacerlo llenando la forma en [http://slack.openmined.org](http://slack.openmined.org)\n",
    "\n",
    "### ¡Únete a un Proyecto de Programación!\n",
    "\n",
    "¡La mejor manera de contribuir a nuestra comunidad es haciéndote un contribuidor de código! Puedes ir a PySyft Github Issues en cualquier momento y filtrar por \"Projects\". Esto te mostrará todos los Tickets de alto nivel, dando un resumen de los proyectos a los que puedes unirte. Si no quieres unirte a un proyecto, pero te gustaría programar un poco, puedes buscar mini-proyectos únicos buscando en Github Issues con \"good first issue\".\n",
    "\n",
    "- [PySyft Projects](https://github.com/OpenMined/PySyft/issues?q=is%3Aopen+is%3Aissue+label%3AProject)\n",
    "- [Good First Issue Tickets](https://github.com/OpenMined/PySyft/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22)\n",
    "\n",
    "### Donaciones\n",
    "\n",
    "Si no tienes tiempo para contribuir a nuestra base de código, pero quieres brindarnos tu apoyo, puedes respaldarnos en nuestro Open Collective. Todas las donaciones van hacia nuestro alojamiento web y otros gastos de la comunidad como hackatones y reuniones. \n",
    "\n",
    "[OpenMined's Open Collective Page](https://opencollective.com/openmined)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}